import csv
import time
from datetime import date, datetime
from utils.misc import MAXFLOAT

from stacking.item import Item
from stacking.action import Action
from stacking.warehouse import Warehouse, Crane, Entrance, Exit, Stack


def parse_csv(filepath, delimiter=",", filters=(), coordinate_identification=True):
    """Main parsing function. This function reads a csv file and creates the appropriate objects 
    for creating instances of the SP and CS problems. It returns a tuple containing a warehouse, 
    a list of items, and a list of actions (the solution contained in the csv file)."""
    actions = []                        # [Action]
    items = dict()                      # {id: Item}
    warehouse = Warehouse(id=filepath)  # Warehouse (groups stacks and cranes)
    
    rows = iter(csv.reader(open(filepath), delimiter=delimiter))
    rows.next()  # ignore the first row (usually contains headers)
    prev_timestamp = -MAXFLOAT
    for i, row in enumerate(rows):
        crane_id = row[0]
        timestamp = parse_datetime(row[1], row[2])
        if timestamp < prev_timestamp:
            print "WARNING: action timestamps out of order at row %d" % i
        action_type = Action.PICKUP if row[3] == "2" else Action.PUTDOWN
        place_id = row[5]
        x = float(row[71]) / 100.0
        y = float(row[72]) / 100.0
        
        # parse Items involved in this action
        nitems = int(row[6])
        moved_items = []
        for j in xrange(7, 70, 7):
            data = row[j:j+7]
            item = parse_item(data, items)
            if item is not None:
                moved_items.append(item)
        assert nitems == len(moved_items)
        
        # create a crane and place for this current action (if necessary)
        crane = warehouse.objects.get(crane_id) or Crane(id=crane_id, initial_xy=(x, y)) 
        place = warehouse.objects.get(place_id) or make_place(place_id, x, y, 
                                                              coordinate_identification)
        
        # create the action and apply filters before adding it to the action list
        action = Action(i, action_type, [item.id for item in moved_items], 
                        place_id, internal=place.is_stack)
        action.meta.crane = crane_id
        action.meta.timestamp = timestamp
        action.meta.xy = (x, y)
        if not all(filter_fnc(action) for filter_fnc in filters):
            continue
        actions.append(action)
        
        # add place and crane to the warehouse (only if the action was accepted by the filters)
        if crane_id not in warehouse.objects:
            warehouse.add(crane)
        if place_id not in warehouse.objects:
            warehouse.add(place)
        prev_timestamp = timestamp
        
    # filter out items that do not appear in any action and sort actions by timestamp
    used_item_ids = set()
    for action in actions:
        used_item_ids.update(action.items)
    items = [item for item_id, item in items.iteritems() if item_id in used_item_ids]
    actions.sort(key=lambda a: a.meta.timestamp)
    # set action ids to reflect the temporal order
    for i, action in enumerate(actions):
        action.id = i
    return warehouse, items, actions
    
    
def parse_item(data, items):
    """This function is given a section of a row in the csv file containing the data concerning a 
    particular item. If this item already exists in 'items', the previously created instance is 
    returned, otherwise a new item is created and added to 'items'."""
    item_id = data[0]
    if item_id.strip() == "":
        return None
    if item_id in items:       # item created before: use the object we already have
        item = items[item_id]
    else:                      # item not found: create a new object
        # are these conversions to meters correct?!?
        width     = float("0" + data[1]) / 1000.0
        length    = float("0" + data[2]) / 1000.0
        thickness = float("0" + data[3]) / 100000.0
        weight    = float("0" + data[4])
        customer  = data[5]
        maxdue    = parse_date(data[6]) if len(data[6]) > 0 else None
        item = items[item_id] = Item(id=item_id, release=None, due=None, weight=weight,
                                     width=width, length=length, thickness=thickness, 
                                     maxdue=maxdue, customer=customer)
    return item
    
    
def make_place(place_id, x, y, coordinate_identification=True):
    """Given the id of a place and its coordinates, this functions creates an Entrance, Stack, or 
    Exit object. Two modes can be used to specify how the function classifies places: 
        1) coordinate identification, which uses hard-coded coordinates to classify places.
        2) identification by id type, which uses a set of rules based on the given id to classify 
           a place: tables start with T, exits have integer ids < 1000, and everything else is a 
           stack.
    """
    if coordinate_identification:
        if  59.45 <= x <= 72.00 \
        or  82.25 <= x <= 90.30 \
        or 146.30 <= x <= 174.15 \
        or 205.00 <= x <= 215.00:
            place = Entrance(id=place_id, xy=(x, y))
        elif x <= 24.35 or x >= 305.60:
            place = Exit(id=place_id, xy=(x, y))
        else:
            place = Stack(id=place_id, xy=(x, y))
    else:
        # place ids starting with T represent tables
        if place_id[0] == "T":
            place = Entrance(id=place_id, xy=(x, y))
        else:
            try:
                place_id = int(place_id)
            except ValueError:
                pass
            # pallets (delivery points) have numeric place id < 1000
            if isinstance(place_id, int) and place_id < 1000:
                place = Exit(id=str(place_id), xy=(x, y))
            else:
                place = Stack(id=str(place_id), xy=(x, y))
    return place
    
    
def parse_date(date_str):
    """Parse a date in the format YYYY/MM/DD and return a number representing the number of 
    seconds since the epoch (like time.time())."""
    year, month, day = [int(num) for num in date_str.split("/")]
    return time.mktime(date(year, month, day).timetuple())
    
    
def parse_datetime(date_str, time_str=""):
    """Parse a date in the format 'YYYYMMDD' and a time in the format '[[hh]mm]ss' and return a 
    number representing the number of seconds since the epoch (like time.time())."""
    year   = int(date_str[:4])
    month  = int(date_str[4:6])
    day    = int(date_str[6:])
    if len(time_str) == 0:
        hour   = 0
        minute = 0
        second = 0
    else:
        second = int(time_str[-2:])
        minute = 0 if len(time_str) <= 2 else int(time_str[-4:-2])
        hour   = 0 if len(time_str) <= 4 else int(time_str[:-4])
    return time.mktime(datetime(year, month, day, hour, minute, second).timetuple())
    
    
def filter_cranes(*crane_ids):
    """Creates a filter function suitable for use with parse_csv(). Filters actions involving the 
    argument crane ids."""
    crane_ids = set(crane_ids)
    def filter_fnc(action):
        return action.meta.crane in crane_ids
    return filter_fnc
    
    
# this filter should be used with file "large.csv"    
default_filter = filter_cranes("114", "115", "116")

